package butines.core;



import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.sound.sampled.AudioFileFormat.Type;
import javax.sound.sampled.AudioFormat.Encoding;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

public class AudioManager {

	private Type typeAudio;
	private AudioFormat playbackFormat;
	private int numLine;
	
	private LinkedList<Sound> queue;
	private ExecutorService executor;
	private boolean alive;
	
	private Map<String, Sound> cache;
	
	private int maxConcurrentSoundsRepeated = 5;
	
	public AudioManager() {
		this.queue = new LinkedList<Sound>();
		this.cache = new HashMap<String, Sound>();
	}
	
	public void init(AudioFormat playbackFormat, Type typeAudio, int numLine) {
	    if (!alive) {
    	    this.alive = true;
    	    this.playbackFormat = playbackFormat;
            this.typeAudio = typeAudio;
            this.numLine = numLine;
            this.executor = Executors.newFixedThreadPool(numLine);
            for (int i = 0; i < numLine; i++) {
                executor.execute(new Worker());
            }
	    }
	}

	public void init(float sampleRate, Type typeAudio, int numLine) {
        init(new AudioFormat(Encoding.PCM_SIGNED, sampleRate, 16, 2, 4, sampleRate, false), typeAudio, numLine);
	}
	
	public void shutdown() {
		if (alive) {
			synchronized(this) {
				alive = false;
				queue.clear();
				notifyAll();
			}
			executor.shutdown();
			cache.clear();
		}
	}
	
	public AudioFormat getPlaybackFormat() {
		return playbackFormat;
	}
	
	public Type getTypeAudio() {
		return typeAudio;
	}

	public int getNumLine() {
		return numLine;
	}

	public boolean isAlive() {
		return alive;
	}
	
	public void loadSound(String path) {
		if (cache.containsKey(path)) {
			return;
		}
		try {
			byte[] samples = readSamples(new File(path), playbackFormat, typeAudio);
			Sound sound = new Sound(samples);
			cache.put(path, sound);
		} catch (Exception e) {
			throw new RuntimeException("Error on load sound", e);
		}
	}

	public void removeSound(String path) {
		cache.remove(path);
	}
	
	public synchronized void playSound(String path, boolean blocking) {
		if (alive == false) {
			throw new IllegalStateException("shutdown is call");
		}

		loadSound(path);
		
		Sound sound = cache.get(path);
		
		if ((blocking && sound.playing > 0)
	            || (sound.playing >= maxConcurrentSoundsRepeated)) {
			return;
		}
		
		queue.add(sound);
		notify();
	}
	
	private synchronized Sound getSound() throws InterruptedException {
        while (queue.size() == 0) {
        	if (alive == false) {
                return null;
            }
            wait();
        }
        return (Sound)queue.removeFirst();
    }

	protected class Worker implements Runnable {

    	private int bufferSize;
		private byte[] buffer;
		private SourceDataLine line;
		
		public Worker() {
			// use a short, 100ms (1/10th sec) buffer for real-time change to the sound stream
			int frameSize = playbackFormat.getFrameSize();
			int sampleRate = Math.round(playbackFormat.getSampleRate() / 10);
			bufferSize = frameSize * sampleRate;
			buffer = new byte[bufferSize];
		}

		private void init() throws LineUnavailableException {
			// create a line to play to
			DataLine.Info info = new DataLine.Info(SourceDataLine.class, playbackFormat);
			line = (SourceDataLine) AudioSystem.getLine(info);
			line.open(playbackFormat, bufferSize);
			// start the line
			line.start();
		}
		
		private void destory() {
			// wait until all data is played
			line.drain();
			// close the line
			line.close();
		}
		
		public void run() {
			try {
				init();
				
				while (alive) {
	                // get a sound to play
	                Sound sound = null;
	                try {
	                    sound = getSound();
	                } catch (InterruptedException ex) {
	                	// ignored
	                }
	                // if getSound() returned null or was interrupted, close this thread.
	                if (sound == null) {
	                    break;
	                }
	                // play the sound, and eat any exceptions it throws
	                try {
	                	InputStream iStream = new ByteArrayInputStream(sound.samples);
	                	sound.play(line, buffer, iStream);
	                } catch (Throwable t) {
	                    t.printStackTrace();
	                }
	            }
				
				destory();
			} catch (Exception e) {
				e.printStackTrace();
			}

		}
		
	}

	private class Sound {

		byte[] samples;
		int playing;
		
		private Sound(byte[] samples) {
			this.samples = samples;
		}
		
		protected void play(SourceDataLine line, byte[] buffer, InputStream iStream){
			try {
				playing++;
				int numBytesRead = 0;
				while (numBytesRead != -1) {
					numBytesRead = iStream.read(buffer, 0, buffer.length);
					if (numBytesRead != -1) {
						line.write(buffer, 0, numBytesRead);
					}
				}
			} catch (IOException ex) {
				ex.printStackTrace();
			} finally {
			    playing--;
			}
		}

	}
	
	public static boolean isPcm(Encoding encoding) {
		return encoding == Encoding.PCM_SIGNED
//			|| encoding == Encoding.PCM_FLOAT
			|| encoding == Encoding.PCM_UNSIGNED;
	}
	
	public static boolean isSameFormat(AudioFormat a, AudioFormat b) {
		return a.getChannels() == b.getChannels()
			&& a.getEncoding() == b.getEncoding()
			&& a.getFrameRate() == b.getFrameRate()
			&& a.getFrameSize() == b.getFrameSize()
			&& a.getSampleRate() == b.getSampleRate()
			&& a.getSampleSizeInBits() == b.getSampleSizeInBits();
	}

	public static byte[] readFully(AudioInputStream aiStream, AudioFormat format) throws IOException {
		long frameLength = aiStream.getFrameLength();
		int frameSize = format.getFrameSize();

		// get the number of bytes to read
		int length = (int) (frameLength * frameSize);

		// read the entire stream
		byte[] samples = new byte[length];
		DataInputStream is = new DataInputStream(aiStream);
		is.readFully(samples);

		// return the samples
		return samples;
	}

	public static AudioInputStream convertFormat(AudioInputStream sourceStream, AudioFormat targetFormat, Type targetType)
		throws IOException, UnsupportedAudioFileException {

		AudioInputStream targetStream = AudioSystem.getAudioInputStream(targetFormat, sourceStream);

		File tmp = File.createTempFile("sndc", null);
		tmp.deleteOnExit();
		try {

			AudioSystem.write(targetStream, targetType, tmp);

			FileInputStream fiStream = new FileInputStream(tmp);
			BufferedInputStream biStream = new BufferedInputStream(fiStream);
			AudioInputStream aiStream = AudioSystem.getAudioInputStream(biStream);

			return aiStream;
		} finally {
			tmp.delete();
		}

	}
	
	public static byte[] readSamples(File sourceFile, AudioFormat playbackFormat, Type typeAudio)
		throws UnsupportedAudioFileException, IOException {
		
		AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(sourceFile);
		if (fileFormat.getType() != typeAudio) {
			throw new IllegalArgumentException("Invalid Type Audio");
		}

		AudioInputStream aiStream = AudioSystem.getAudioInputStream(sourceFile);
		AudioFormat format = aiStream.getFormat();

		try {
			if (isPcm(format.getEncoding()) == false) {
				throw new IllegalArgumentException("Encoding of source audio data is not PCM; conversion not possible");
			}
		
			if (isSameFormat(playbackFormat, format) == false) {
				aiStream = convertFormat(aiStream, playbackFormat, typeAudio);
			}
			
			return readFully(aiStream, playbackFormat);
		} finally {
			aiStream.close();
		}
	}
	
}
